Fork me on GitHub

Import the token vocabulary described by the MellowD Lexer

options {
	tokenVocab = MellowDLexer;
}

Declare all of the imports needed. These go in the class header

@header {
    import cas.cs4tb3.mellowd.*;
    import cas.cs4tb3.mellowd.midi.*;
    import cas.cs4tb3.mellowd.primitives.*;
    import javax.sound.midi.*;
    import java.util.*;
}

The @members block declares code that should be defined inside the generated parser class.

@members {

The evaluation of everything depends on BlockOptions and the global options will serve as the options for variable evaluation.

    private BlockOptions globalOptions = new BlockOptions();

The symbol table will be a reference table for all variable declarations

    private SymbolTable symbolTable = new SymbolTable();

The timing environment is described in the compiler arguments and dictates the duration of notes.

    private TimingEnvironment timingEnv;

The entire goal of the compiler is to build this midiSequence. This sequence stores the actual MIDI data.

    private Sequence midiSequence;

The track manager is mainly used to instantiate blocks but also manages these blocks which wrap a MIDI track.

    private TrackManager trackManager;

An additional constructor (the default should not be used but we can’t get rid of it)

    public MellowDParser(TokenStream inputStream, TimingEnvironment timingEnv, TrackManager trackManager) {
        this(inputStream);
        this.timingEnv = timingEnv;
        this.trackManager = trackManager;
        this.midiSequence = timingEnv.createSequence();
    }

    public BlockOptions getGlobalOptions() {
        return this.globalOptions;
    }

    public Sequence getSequence() {
        return this.midiSequence;
    }

    private int getOctaveShiftRequired(BlockOptions options) {
        if (options.isPercussion()) return 0;
        return options.getOctave() - this.globalOptions.getOctave();
    }

Chords are resolved differently then a direct symbol table lookup. There are a standard set of names for chords and we want to lookup these names as if they exist in the symbol table without actually putting them in there.

    private Chord lookupChord(Token identifier, BlockOptions options) {

Preforming a type check will throw an exception if the identifier is defined for something other than a chord. This means if the identifier is defined as a chord or is undefined the program will continue.

        this.symbolTable.checkType(identifier, Chord.class);

The value in the table is pulled out of the table. If this value is not null then a chord definition exists so we can simply put it in the correct octave and return it.

        Chord chord = this.symbolTable.getDeclarationValue(identifier.getText(), Chord.class);
        if (chord != null) return chord.shiftOctave(this.getOctaveShiftRequired(options));

Otherwise the reference is not defined in the symbol table so try to resolve one of the default chords based on the name.

        chord = Chord.resolve(identifier.getText(), options.getOctave());

If the chord is still null then it could not be resolved and we should therefor throw an undefined reference exception.

        if (chord == null)
            throw new UndefinedReferenceException(identifier, "Identifier ("+identifier.getText()+") is undefined.");

Otherwise the resolution was successful and the chord can be returned.

        return chord;
    }

    private GeneralMidiPercussion resolvePercussionID(Token identifier, Token indexNum) {
        GeneralMidiPercussion drumSound = GeneralMidiPercussion.lookup(identifier.getText());

If the sound is not null then it was successfully resolved.

        if (drumSound != null) {

If NUMBER is not null and the percussion sound was resolved we have a semantic issue. A single sound cannot be indexed.

            if (indexNum != null)
                throw new ParseException(indexNum, "Cannot index a percussion sound.");
        }
        return drumSound;
    }

    private MidiNoteMessageSource resolveChordID(Token identifier, Token indexNum, BlockOptions options) {
        Chord chord = lookupChord(identifier, options);

If the indexNum is null there is no indexing and we can return the chord.

        if (indexNum == null) {
            return chord;

Otherwise attempt to pull a single pitch out of the chord.

        } else {

The indexNum is a valid int because the pattern that is must match to become the NUMBER token is an int description.

            int index = Integer.parseInt(indexNum.getText());
            if (index > chord.size())
                throw new ParseException(identifier, "Index "+index+" is larger than the chord size of "+chord.size());
            return chord.getPitch(index);
        }
    }

    private void resolveChordParamID(List<Pitch> pitches, Token identifier, Token indexNum, BlockOptions options) {

If inside a percussion block, first attempt to resolve the identifier as the name of a percussion sound.

        if (options.isPercussion()) {
            GeneralMidiPercussion drumSound = resolvePercussionID(identifier, indexNum);
            if (drumSound != null) {
                pitches.add(drumSound.getAsPitch());
                return;
            }
        }

If pitches is still null then it was not resolved to a percussion sound. Now whether or not we are in a percussion block the next step is to try and resolve a chord from the identifier.

        MidiNoteMessageSource noteSource = resolveChordID(identifier, indexNum, options);
        if (noteSource instanceof Chord) pitches.addAll(((Chord) noteSource).getPitches());
        else                             pitches.add((Pitch) noteSource);
    }

    private void resolveMelodyParamID(Melody root, Token identifier, Token indexNum, Articulation art, BlockOptions options) {

If inside a percussion block, first attempt to resolve the identifier as the name of a percussion sound.

        if (options.isPercussion()) {
            GeneralMidiPercussion drumSound = resolvePercussionID(identifier, indexNum);
            if (drumSound != null) {
                root.add(new ArticulatedSound(drumSound.getAsPitch(), art));
                return;
            }
        }

A percussion sound was not resolved so try and lookup the data pointed to by the identifier. If the type of the pointer is a melody then get the data and append it to the root.

        if (this.symbolTable.identifierTypeIs(identifier.getText(), Melody.class)) {
            Melody mel = this.symbolTable.getDeclarationValue(identifier.getText(), Melody.class);
            mel = mel.shiftOctave(this.getOctaveShiftRequired(options));
            root.add(mel);
            if (indexNum != null)
                throw new IncorrectTypeException(identifier, "Cannot index a melody.");
            if (art != Articulation.NONE)
                throw new IncorrectTypeException(identifier, "Cannot articulate a melody with "+art.name().toLowerCase());

Otherwise attempt to resolve the identifier as a chord and add the chord as a single sound.

        } else {
            MidiNoteMessageSource noteSource = resolveChordID(identifier, indexNum, options);
            root.add(new ArticulatedSound(noteSource, art));
        }
    }
}

Begin defining the parser rules.

An octaveShift moves the note up or down an number of octaves. It matches the pattern for a java integer so the string matched by this rule can be passed directly into Integer.parseInt and doesn’t need to return anything.

octaveShift
    : ( PLUS | MINUS ) NUMBER;

Articulation is a single articulation character. This rule returns the described Articulation

articulation
returns [Articulation art]
    : DOT           {$art = Articulation.STACCATO;      }
    | EXCLAMATION   {$art = Articulation.STACCATISSIMO; }
    | HAT           {$art = Articulation.MARCATO;       }
    | BACK_TICK     {$art = Articulation.ACCENT;        }
    | USCORE        {$art = Articulation.TENUTO;        }
    | TILDA         {$art = Articulation.GLISCANDO;     }
    ;

A noteChar is a single lowercase letter from A to G that describes a ptich. This pitch is different depending on the octave so the resolution requires some BlockOptions to obtain the octave for the current context.

noteChar[BlockOptions options]
returns [Pitch pitch]
    : A     {$pitch = Pitch.A.inOctave($options.getOctave());}
    | B     {$pitch = Pitch.B.inOctave($options.getOctave());}
    | C     {$pitch = Pitch.C.inOctave($options.getOctave());}
    | D     {$pitch = Pitch.D.inOctave($options.getOctave());}
    | E     {$pitch = Pitch.E.inOctave($options.getOctave());}
    | F     {$pitch = Pitch.F.inOctave($options.getOctave());}
    | G     {$pitch = Pitch.G.inOctave($options.getOctave());}
    ;

A noteDef fully describes a pitch. It consists of a noteChar optionally followed by a sharp or flat symbol and an optional octaveShift. This is the top level pitch resolution rule.

noteDef[BlockOptions options]
returns [Pitch pitch]
    :   ( noteChar[$options]    {$pitch = $noteChar.pitch;}
            (   SHARP           {$pitch = $pitch.sharp(); }
            |   FLAT            {$pitch = $pitch.flat();  }
            )?
            ( octaveShift {$pitch = $pitch.shiftOctave(Integer.parseInt($octaveShift.text));} )?
        )
    ;

A chord definition is one or more chordParams between ( and ) seperated by commas. A chord can be articulated which is the equivalent of articulating each pitch in the chord with the articulation. No individual note articulation is accepted.

chord[BlockOptions options]
returns [ArticulatedSound sound]
locals [List<Pitch> pitches = new LinkedList<>()]
    :   PAREN_OPEN
        chordParam[$options, $pitches] ( COMMA chordParam[$options, $pitches] )*
        PAREN_CLOSE {$sound = new ArticulatedSound(new Chord($pitches));}
        ( articulation {$sound.setArticulation($articulation.art);} )?
    ;

A chord param can be a note or a pointer to another chord that is optionally indexed. As this param may consist of multiple pitches the rule returns a list of pitches. Chord may be indexed for their individual pitches so the order of the pitches is important and the list is the collection required to accomplish this.

chordParam[BlockOptions options, List<Pitch> pitches]
    :   noteDef[$options]
            { $pitches.add($noteDef.pitch); }
    |   IDENTIFIER ( COLON NUMBER )?
            { resolveChordParamID($pitches, $IDENTIFIER, $NUMBER, $options); }
    ;

A melody is made up of 1 or more melodyParams seperated by a comma. Each melodyParam is responsible for appending itself to the melody. The melody definition begins with a [ and ends with a ].

melody[BlockOptions options]
returns [Melody mel = new Melody(new LinkedList<ArticulatedSound>())]
    :   BRACKET_OPEN
        melodyParam[$mel, $options] ( COMMA melodyParam[$mel, $options] )*
        BRACKET_CLOSE
    ;

Each melody parameter is an articulated note, a chord, a pointer to a melody or chord, or a STAR for a rest. Depending on the option matched this rule may add one or many sounds to the melody. The * star character representing a rest.

melodyParam[Melody mel, BlockOptions options]
locals [Articulation art = Articulation.NONE]
    :   noteDef[$options] ( articulation {$art = $articulation.art;} )?
            { $mel.add(new ArticulatedSound($noteDef.pitch, $art)); }
    |   IDENTIFIER ( COLON NUMBER )? ( articulation {$art = $articulation.art;})?
            { this.resolveMelodyParamID($mel, $IDENTIFIER, $NUMBER, $art, $options); }
    |   chord[$options]
            { $mel.add($chord.sound); }
    |   STAR
            { $mel.add(new ArticulatedSound(Pitch.REST)); }
    ;

A rhythm char is a single char that is the irst letter of the beat duration it is describing. The supported durations are whole, half, quarter, eight, sizteenth and thirty-second notes.

rhythmChar
returns [Beat beat]
    : W {$beat = Beat.WHOLE;        }
    | H {$beat = Beat.HALF;         }
    | Q {$beat = Beat.QUARTER;      }
    | E {$beat = Beat.EIGHTH;       }
    | S {$beat = Beat.SIXTEENTH;    }
    | T {$beat = Beat.THIRTYSECOND; }
    ;

A rhythmDef is the building block of a rhythm. It is a rhythm char followed by 0 or more .. Each dot extends the duration of the beat by half its value. This extension uses the previously added value in the calculation. Ex: h.. is 12 + 14 + 18

rhythmDef
returns [Beat beat]
    : rhythmChar ( DOT )* {$beat = $DOT == null ? $rhythmChar.beat : $rhythmChar.beat.dot($DOT.text.length());};

rhythm is the top level rhythm rule. It is a list of rhythmParam‘s seperated by a comma opening with a < and closed with >. By default the rhythmParam‘s inside this definition is not slurred, hence slur=false is passed into the rule call.

rhythm
returns [Rhythm rhy = new Rhythm()]
    : P_BRACKET_OPEN rhythmParam[$rhy, false] ( COMMA rhythmParam[$rhy, false] )* P_BRACKET_CLOSE ;

A slurred rhythm is a regular rhythm inside ( and ). It makes the melody glide over the rhythm without as distinct of a break as regular note performances. It looks the same as rhythm passing slur=true into the rhythmParam rule.

slurredRhythm[Rhythm rhy]
    : PAREN_OPEN rhythmParam[$rhy, true] ( COMMA rhythmParam[$rhy, true] )* PAREN_CLOSE ;

A tuplet is a duration modification. The common tuplet being a triplet. A quarter note triplet performs 3 quarter notes in the same time that normally takes 2. A 5:3 quarter note tuplet performs 5 quarter notes in the time it takes to perform 3. If the second number in the ratio is not given it is assumed to be 1 less than the first. As such a numerator of [0, 1] or a denominator of [0] do not make any sense in this context.

To slur the notes in the tuplet the rhythmDef is wrapped in ( and ). Each tuplet can only consist of beats of the same duration so there is no reason to write the beat out multiple times. It is therefore only written once but adds num beats to the rhythm.

tuplet[Rhythm rhy]
    : num=NUMBER ( COLON div=NUMBER )?
        ( PAREN_OPEN rhythmDef PAREN_CLOSE
        | rhythmDef
        )
        {

Check the preconditions on the num and div

            if ($num.int == 0)
                throw new ParseException($num, "Tuplet number must be greater than 0");
            if ($div != null && $div.int == 0)
                throw new ParseException($div, "Tuplet division cannot be 0");

If the PAREN_OPEN is present then the tuplet is slurred

            boolean slur = $PAREN_OPEN != null;

Expand the beat to the number of beats in the numerator

            for (int i = 0; i < $num.int; i++) {
                Beat beat;

If the division is present use it in the duration calculation

                if ($div != null)
                    beat = $rhythmDef.beat.tuplet($num.int, $div.int);

Otherwise use the default (num only) duration calculation

                else
                    beat = $rhythmDef.beat.tuplet($num.int);

Append the beat to the rhythm

                rhy.append(beat, slur);
            }
        }
    ;

A rhythmParam takes any rhythm parameter and appends the appropriate beats to the rhythm it belongs to. The slur argument specifies if this parameter is slurred or not. Each option in this rule appends the appropriate beats to the rhythm.

rhythmParam[Rhythm rhy, boolean slur]
    :   rhythmDef     { $rhy.append($rhythmDef.beat, $slur); }
    |   IDENTIFIER    {
                        Rhythm value = this.symbolTable.getDeclarationValueOrThrow($IDENTIFIER, Rhythm.class);
                        $rhy.append(value, $slur);
                      }
    |   slurredRhythm[$rhy]
    |   tuplet[$rhy]
    ;

A variable declaration maps an identifier to a primitive. These primitives include Chord , Melody, Rhythm and Phrase. Each mapping is put into the compiler’s symbol table. Adding a * after the assignment token parses the value as if it was inside a percussion block.

varDeclaration
locals [boolean wasPercussion]
@init  {$wasPercussion = this.globalOptions.isPercussion();}
    : id=IDENTIFIER ASSIGNMENT ( STAR {this.globalOptions.setPercussion(true);} )?
                                ( ref=IDENTIFIER                {
                                                                    Object val = this.symbolTable.getDeclarationValue($ref.text, Object.class);
                                                                    if (val != null)
                                                                        this.symbolTable.addDeclaration($id.text, val);
                                                                    else
                                                                        this.symbolTable.addDeclaration($id.text, lookupChord($ref, this.globalOptions));
                                                                }
                                | chord [this.globalOptions]    {this.symbolTable.addDeclaration($id.text, $chord.sound.getSound());}
                                | melody[this.globalOptions]    {this.symbolTable.addDeclaration($id.text, $melody.mel);}
                                | rhythm                        {this.symbolTable.addDeclaration($id.text, $rhythm.rhy);}
                                | phrase[this.globalOptions]    {this.symbolTable.addDeclaration($id.text, $phrase.phr);}
                                ) {this.globalOptions.setPercussion($wasPercussion);}
    ;

Dynamics are what change the velocity of a note. Mellow D supports the main dynamic identifiers with pppp being the quietest and ffff being the loundest.

dynamicDeclaration
returns [Dynamic dynamic]
    : PPPP  {$dynamic = Dynamic.pppp;}
    | PPP   {$dynamic = Dynamic.ppp; }
    | PP    {$dynamic = Dynamic.pp;  }
    | P     {$dynamic = Dynamic.p;   }
    | MP    {$dynamic = Dynamic.mp;  }
    | MF    {$dynamic = Dynamic.mf;  }
    | F     {$dynamic = Dynamic.f;   }
    | FF    {$dynamic = Dynamic.ff;  }
    | FFF   {$dynamic = Dynamic.fff; }
    | FFFF  {$dynamic = Dynamic.ffff;}
    ;

Block options are the configuration for the block. They appear after a block identifier inbetween [ and ]. Each configuration option is either a property option or a flag option. The entire configuration is a comma seperated list of these options.

blockOptions[BlockOptions options]
    : BRACKET_OPEN ( ( propertyOption[$options] | flagOption[$options] ) ( COMMA ( propertyOption[$options] | flagOption[$options] ) )* )? BRACKET_CLOSE;

The first option type is a property option. This option assigns a value to a key. The value can be an identifier or a number.

propertyOption[BlockOptions options]
    : key=IDENTIFIER ASSIGNMENT ( valueID=IDENTIFIER | valueNum=NUMBER )
        {   switch($key.text.toLowerCase()) {

An instrument name or MIDI number can be specified to change the instrument for this block.

                case "instrument":
                    if ($valueID != null) {
                        $options.setInstrument($valueID);
                    } else {
                        int inst = $valueNum.int;
                        if (inst > 127)
                            throw new ParseException($valueNum, "Instrument code must be less than 128.");
                        $options.setInstrument(inst);
                    }
                    break;

A soundbank can only be given by MIDI number. This option is not used in the majority of cases but allows support for custom synthesiser banks

                case "soundbank":
                    if ($valueNum == null)
                        throw new ParseException($valueID, "Expected a soundbank id but found "+$valueID.text);
                    $options.setSoundbank($valueNum.int);
                    break;

The octave is the base octave for the block. All declarations inside a block are relative to this octave. As an octave is a number, an identifier is not an acceptable.

                case "octave":
                    if ($valueNum == null)
                        throw new ParseException($valueID, "Expected octave number but found "+$valueID.text);
                    int octave = $valueNum.int;
                    if (octave > 10) throw new ParseException($valueNum, "Octave is too high. 10 is highest byt found "+octave);
                    $options.setOctave(octave);
                    break;

Loop or repeat specifies how many times to repeat the contents of the block in the compiled song.

                case "loop":
                case "repeat":
                    if ($valueNum == null)
                        throw new ParseException($valueID, "Expected a loop count but found "+$valueID.text);
                    $options.setLoopCount($valueNum.int);
                    break;

onchannel, samechannelas, sharechannel are all ways to say that this block should operate on the same channel as another block by the given name. As all block names are identifiers it does not make sense for this value to be a number.

                case "onchannel":
                case "samechannelas":
                case "sharechannel":
                    if ($valueID == null)
                        throw new ParseException($valueNum, "Expected the name of the block that this channel should share a channel with"
                            + " but found " + $valueNum.text);
                    $options.setShareChannel($valueID.text);
                    break;

channel is another option that is uncommon. It allows direct specification of the MIDI channel number that this block should operate on. As it is a number, an identifier does not make sense in this context.

                case "channel":
                    if ($valueNum == null)
                        throw new ParseException($valueID, "Expected the number of the MIDI channel to requests but"
                            + " found " + $valueID.text);
                    $options.setChannel($valueNum.int);
                    break;

If the key was not caught by another case it is an unknown option.

                default:
                    throw new ParseException($key, "Unknow block option " + $key.text + ".");
            }
        }
    ;

A flag option is an on/off flag. If the flag exists it is in the on state. If it is prefixed with - it is set to the off state.

flagOption[BlockOptions options]
    :   ( off=MINUS )? flag=IDENTIFIER
        {
            switch ($flag.text.toLowerCase()) {

percussion or drums sets the block in percussion mode where it can accept percussion sound names to describe pitches.

                case "percussion":
                case "drums":
                    $options.setPercussion($off == null);
                    break;

If none of the cases catch the flag then the option is undefined.

                default:
                    throw new ParseException($flag, "Unknow block option " + $flag.text + ".");
            }
        }
    ;

Phrases are the finished product for a sequence of sounds. A phrase may be a pointer to a phrase variable or a pitch definition * a rhythm. A pitch definition may be a melody, chord, or a pointer to a melody or chord. The rhythm may be a direct rhythm declaration or a pointer to a rhythm.

phrase[BlockOptions options]
returns [Phrase phr]
locals [Melody mel]
    :   (   ( melody[$options]  {$mel = $melody.mel;}
            | chord[$options]   {$mel = new Melody(Arrays.asList($chord.sound));}
            | IDENTIFIER articulation?
                            {

If the identifier points to a melody use it as the melody

                                if (this.symbolTable.identifierTypeIs($IDENTIFIER.getText(), Melody.class)) {
                                    $mel = this.symbolTable.getDeclarationValue($IDENTIFIER.getText(), Melody.class).shiftOctave(this.getOctaveShiftRequired($options));

A melody cannot be articulated so throw an exception if an articulation was given.

                                    if ($articulation.ctx != null)
                                        throw new IncorrectTypeException($IDENTIFIER, "Cannot articulate a melody.");

Otherwise attempt to resolve a chord. This resolution will throw an exception if it can’t be resolved.

                                } else {
                                    Chord chord = lookupChord($IDENTIFIER, $options);

Build a melody with a single element, the chord.

                                    List<ArticulatedSound> notes = Arrays.asList(new ArticulatedSound(chord, $articulation.ctx == null ? Articulation.NONE : $articulation.art));
                                    $mel = new Melody(notes);
                                }
                            }
            )
            STAR
            ( rhythm {$phr = $rhythm.rhy.createPhrase($mel);}

Try to resolve the identifier as a rhythm and create a phrase from it

            | IDENTIFIER {$phr = this.symbolTable.getDeclarationValueOrThrow($IDENTIFIER, Rhythm.class).createPhrase($mel);}
            )
        )
        |

The other option is to specify a variable name that points to a phrase

        ( IDENTIFIER {$phr = this.symbolTable.getDeclarationValueOrThrow($IDENTIFIER, Phrase.class).shiftOctave(this.getOctaveShiftRequired($options));} )
    ;

A block is a collection of phrases and dynamic declarations.

block
locals [Block b, BlockOptions options]

The first step is to lookup the block with the given name ($IDENTIFIER). If the block doesn’t exist yet the block options used will be a copy of the global options. Otherwise the options will be a copy of the block’s options.

    :   IDENTIFIER {$b = this.trackManager.getBlock($IDENTIFIER.text); $options  = new BlockOptions($b == null ? this.globalOptions : $b.getOptions());} blockOptions[$options]? BRACE_OPEN
            {

If the block is null it must be created now with the parsed block options.

                if ($b == null)
                    $b = this.trackManager.createBlock($IDENTIFIER, this.timingEnv, this.midiSequence.createTrack(), $options);

Then enter the block

                try {
                    $b.enterBlock($IDENTIFIER, $options);
                } catch (InvalidMidiDataException e) {
                    throw new ParseException($blockOptions.start, e.getMessage());
                }
            }

Now that we are inside the block we expect to see dynamicDeclarations or a phrases.

        (

A dynamic declaration sets the dynaic for the block it is currently inside. If it is directly followed by a crescendo or decrescendo token then the block dynamic is notified that the change to the next token shuld be gradual

            dynamicDeclaration  {
                                    try {
                                        $b.setDynamic($dynamicDeclaration.dynamic);
                                    } catch (InvalidMidiDataException e) {
                                        throw new ParseException($dynamicDeclaration.start, e.getMessage());
                                    }
                                }
            ( DYNAMIC_CRES      {$b.crescendo($DYNAMIC_CRES);}
            | DYNAMIC_DECRES    {$b.decrescendo($DYNAMIC_DECRES);}
            )?

If a phrase is encountered it is added to the block which appends it to the track.

        | phrase[$options]    {
                                            try {
                                                $b.addPhrase($phrase.phr);
                                            } catch (InvalidMidiDataException e) {
                                                throw new ParseException($dynamicDeclaration.start, e.getMessage());
                                            }
                                        }
        )*

When the block is closed with the closing } the block is notified that no more data will be incoming in this fragment.

        BRACE_CLOSE {$b.leaveBlock();}
    ;

A song is the top level rule, the entry point for the parser. At the top level only variable declarations or blocks can be defined. A song consists of any number of these declarations.

song
    :   ( varDeclaration
        | block
        )*
        EOF {this.trackManager.finish();}
    ;
h